using System.Configuration;
using System.IO;
using MLSharp.Classification;
using MLSharp.Filters;

namespace MLSharp.Clustering
{
	/// <summary>
	/// Interface to the knnflex nearest neighbor library for R.
	/// </summary>
	public class KnnFactory : IClassifierFactory
	{
		#region Logger

		/// <summary>
		/// The logger for the class.
		/// </summary>
		private static readonly log4net.ILog mLogger =
			log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);

		#endregion

		#region Private Fields

		/// <summary>
		/// The path to R.exe.
		/// </summary>
		private readonly string mRPath;

		/// <summary>
		/// The path to the working directory.
		/// </summary>
		private readonly string mWorkingDirectory;

		#endregion

		#region Public Properties

		/// <summary>
		/// The options to use when building nearest neighbors.
		/// </summary>
		public KnnOptions Options { get; private set; }

		/// <summary>
		/// Wrapper for Options.
		/// </summary>
		object IClassifierFactory.Options
		{
			get { return this.Options; }
		}

		#endregion

		#region Public Constructors

		/// <summary>
		/// Creates a factory.
		/// </summary>
		/// <remarks>
		/// You can configure R using the app.config file.  You can specify:
		/// <list>
		///		<item>RPath - the full path to R.exe.</item>
		///		<item>RWorkingDirectory - A directory to place intermediate output into.</item>
		/// </list>
		/// </remarks>
		public KnnFactory()
		{
			mRPath = ConfigurationManager.AppSettings["RPath"] ?? Path.GetFullPath("R.exe");
			mWorkingDirectory = ConfigurationManager.AppSettings["RWorkingDirectory"] ?? Path.GetFullPath(@".\R");

			Initialize();
		}

		/// <summary>
		/// Creates a factory using the specified paths.
		/// </summary>
		/// <param name="rPath">The path to R.exe.</param>
		/// <param name="workingDirectory">The path to the working directory.</param>
		public KnnFactory(string rPath, string workingDirectory)
		{
			mRPath = Path.GetFullPath(rPath);
			mWorkingDirectory = Path.GetFullPath(workingDirectory);

			Initialize();
		}

		#endregion

		#region Private Static Methods

		#endregion

		#region Private Methods

		/// <summary>
		/// Performs common initialization.
		/// </summary>
		private void Initialize()
		{
			Options = new KnnOptions();

			if (!Directory.Exists(mWorkingDirectory))
			{
				Directory.CreateDirectory(mWorkingDirectory);
			}

			ValidateConfiguration();
		}

		/// <summary>
		/// Verifies that fields have been set to correct, valid values.
		/// </summary>
		private void ValidateConfiguration()
		{
			//Make sure the R executable path is valid.
			if (!File.Exists(mRPath))
			{
				throw new FileNotFoundException(string.Format("Unable to locate R.exe at '{0}'", mRPath));
			}
		}
		#endregion

		#region Public Methods

		/// <summary>
		/// Builds a nearest neighbor that can predict the specified target attribute.
		/// </summary>
		/// <param name="dataSet">The data set to train the network on.</param>
		/// <returns>The nearest neighbor classifier.</returns>
		/// <remarks>
		/// The algorithm *must* include only attributes that can be converted to 
		/// doubles (label attributes can still be arbitrary strings).  You can
		/// use <see cref="StandardizeFilter"/> to standardize your data set.
		/// </remarks>
		public KnnClassifier BuildKnn(IDataSet dataSet)
		{
			//just save dataSet in train for use later
			/*
			//This is the actual R script that will be generated.
			string rScriptName = Path.Combine(mWorkingDirectory, NameGenerator.GetRandomName(10) + ".R");
			//And these are the input variables for the instances
			string rInputTableName = Path.Combine(mWorkingDirectory, NameGenerator.GetRandomName(10) + ".InpTable");
			//And these are the instance target/output variables
			string rTargetTableName = Path.Combine(mWorkingDirectory, NameGenerator.GetRandomName(10) + ".TarTable");

			//Count the number of independent (non-label) attributes that are in the data set.
			int independentAttributeCount = dataSet.CountIndependentAttributes();

			using (StreamWriter writer = File.CreateText(rScriptName))
			{
				//Step 1: Write the header part of an R script.
				writer.WriteLine("library(kknn)");

				//Step 2: Write the training data to a .table file
				RTableWriter.Write(dataSet, rInputTableName, rTargetTableName);

				//Step 3: Write the command to load the inputs table
				writer.WriteLine("train<-read.table(\"{0}\", header = FALSE)", rInputTableName.Replace(@"\", @"\\"));
                //Step 4: Write the command to load the outputs table
                writer.WriteLine("y<-as.matrix(read.table(\"{0}\", header = FALSE))", rTargetTableName.Replace(@"\", @"\\"));

				//This command will train a new network.
				/*string trainNetwork = @"net.start <- newff(n.neurons=c({0}),learning.rate.global={1},
                momentum.global={2},
                error.criterium=""{3}"",
                Stao=NA,
                hidden.layer=""{4}"",
                output.layer=""{5}"",
                method=""{6}"")
                result <- train(net.start, x, y, error.criterium=""{3}"", report=TRUE, show.step={7}, n.shows=10 )
                ";
                 //
                string predict = @"pred<-kknn(formula = formula(train), trian, test,k={0}, distance={1}, kernel=\"{2}\")";

				predict = string.Format(Options.k, Options.distance, Options.kernel);

				if (mLogger.IsInfoEnabled)
				{
					//mLogger.InfoFormat("Training network: {0}", trainNetwork);
                    mLogger.InfoFormat("Starting classifcation: ", predict);
				}

				//Step 6: Write the prediction command to the R script
				writer.WriteLine(predict);

				//Step 7: Write the commands to dump the output
				//writer.WriteLine("dump(\"result\",file=stdout())");
                writer.WriteLine("dump(\"table(test$TCNeed,fitted(pred))\",file=stdout())");
			}

			//Step 8: Execute R on the script, redirecting the output
			ProcessStartInfo startInfo = new ProcessStartInfo(mRPath);
			startInfo.RedirectStandardOutput = true;
			startInfo.RedirectStandardError = true;
			startInfo.WorkingDirectory = mWorkingDirectory;
			startInfo.Arguments = string.Format("-f \"{0}\" --slave", rScriptName);
			//This must be false if you are redirecting input/output
			startInfo.UseShellExecute = false;
			startInfo.CreateNoWindow = true;

			string output;

			using (Process rProcess = Process.Start(startInfo))
			{
				if (rProcess == null)
				{
					throw new NullReferenceException("rProcess is null!");
				}

				output = rProcess.StandardOutput.ReadToEnd();
				rProcess.WaitForExit();

				//You can uncomment this line if you need to see the actual network.
				//Console.WriteLine(output);
			}

			//Step 9: Parse the output and extract the network parameters.

			//There should be 2 lines for the confusion matrix

			//The output had better begin with a network assignment, or something has gone wrong
			if (!output.StartsWith("result <-"))
			{
				throw new InvalidOperationException("Unexpected output from R.exe, begins with '" +
				                                    output.Substring(0, Math.Min(10, output.Length)) + "'");
			}

			//The output should end with '))\r\n'
			if (!output.EndsWith("))\r\n"))
			{
				throw new InvalidOperationException("Unexpected output from R.exe, ends with '" +
				                                    output.Substring(Math.Max(0, output.Length - 10)) + "'");
			}

			//Step 10: Cleanup the temp files.
			File.Delete(rScriptName);
			File.Delete(rInputTableName);
			File.Delete(rTargetTableName);
            */
			//Step 11: Return a new network.
			//don't need output anymore
			//return new Knn(mWorkingDirectory, mRPath, output);
			return new KnnClassifier(mWorkingDirectory, mRPath, dataSet, Options);
		}

		#endregion

		#region IClassifierFactory Members

		/// <summary>
		/// Calls <see cref="BuildKnn"/>.
		/// </summary>
		/// <param name="trainingData"></param>
		/// <returns></returns>
		IClassifier IClassifierFactory.BuildClassifier(IDataSet trainingData)
		{
			return BuildKnn(trainingData);
		}

		#endregion
	}
}
